/*
 * Copyright 2018 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.javascript.jscomp.ClosureCheckModule.DECLARE_LEGACY_NAMESPACE_IN_NON_MODULE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_REQUIRE_NAMESPACE;

import com.google.common.base.Splitter;
import com.google.common.collect.LinkedHashMultiset;
import com.google.javascript.jscomp.deps.ModuleLoader.ModulePath;
import com.google.javascript.jscomp.deps.ModuleLoader.ResolutionMode;
import com.google.javascript.jscomp.modules.ModuleMetadataMap;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleType;
import com.google.javascript.jscomp.parsing.parser.Identifiers;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.QualifiedName;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;

/**
 * Gathers metadata around modules that is useful for checking imports / requires and creates a
 * {@link ModuleMetadataMap}.
 */
public final class GatherModuleMetadata implements CompilerPass {
  static final DiagnosticType MIXED_MODULE_TYPE =
      DiagnosticType.error("JSC_MIXED_MODULE_TYPE", "A file cannot be both {0} and {1}.");

  static final DiagnosticType INVALID_NAMESPACE_OR_MODULE_ID =
      DiagnosticType.error(
          "JSC_INVALID_NAMESPACE_OR_MODULE_ID",
          "Namespace and module ID must be a dot-separated sequence of legal property"
              + " identifiers and must only contain ASCII, 0-9, $, ., and _. Found ''{0}''");

  static final DiagnosticType INVALID_DECLARE_MODULE_ID_CALL =
      DiagnosticType.error(
          "JSC_INVALID_DECLARE_NAMESPACE_CALL",
          "goog.declareModuleId parameter must be a string literal.");

  static final DiagnosticType DECLARE_MODULE_ID_OUTSIDE_ES6_MODULE =
      DiagnosticType.error(
          "JSC_DECLARE_MODULE_NAMESPACE_OUTSIDE_ES6_MODULE",
          "goog.declareModuleId can only be called within ES6 modules.");

  static final DiagnosticType MULTIPLE_DECLARE_MODULE_NAMESPACE =
      DiagnosticType.error(
          "JSC_MULTIPLE_DECLARE_MODULE_NAMESPACE",
          "goog.declareModuleId can only be called once per ES6 module.");

  static final DiagnosticType INVALID_REQUIRE_TYPE =
      DiagnosticType.error(
          "JSC_INVALID_REQUIRE_TYPE", "Argument to goog.requireType must be a string.");

  static final DiagnosticType INVALID_REQUIRE_DYNAMIC =
      DiagnosticType.error(
          "JSC_INVALID_REQUIRE_DYNAMIC", "Argument to goog.requireDynamic must be a string.");

  static final DiagnosticType INVALID_MAYBE_REQUIRE =
      DiagnosticType.error(
          "JSC_INVALID_MAYBE_REQUIRE",
          "Argument to goog.maybeRequireFrameworkInternalOnlyDoNotCallOrElse must be a string.");

  static final DiagnosticType INVALID_SET_TEST_ONLY =
      DiagnosticType.error(
          "JSC_INVALID_SET_TEST_ONLY",
          "Optional, single argument to goog.setTestOnly must be a string.");

  static final DiagnosticType INVALID_NESTED_LOAD_MODULE =
      DiagnosticType.error("JSC_INVALID_NESTED_LOAD_MODULE", "goog.loadModule cannot be nested.");

  static final DiagnosticType INVALID_TOGGLE_USAGE =
      DiagnosticType.error("JSC_INVALID_TOGGLE_USAGE", "Invalid toggle usage: {0}");

  private static final Node GOOG_PROVIDE = IR.getprop(IR.name("goog"), "provide");
  private static final Node GOOG_MODULE = IR.getprop(IR.name("goog"), "module");
  private static final Node GOOG_MODULE_GET =
      IR.getprop(IR.getprop(IR.name("goog"), "module"), "get");
  private static final Node GOOG_REQUIRE = IR.getprop(IR.name("goog"), "require");
  private static final Node GOOG_REQUIRE_TYPE = IR.getprop(IR.name("goog"), "requireType");
  private static final Node GOOG_MAYBE_REQUIRE =
      IR.getprop(IR.name("goog"), "maybeRequireFrameworkInternalOnlyDoNotCallOrElse");
  private static final Node GOOG_REQUIRE_DYNAMIC = IR.getprop(IR.name("goog"), "requireDynamic");
  private static final Node GOOG_SET_TEST_ONLY = IR.getprop(IR.name("goog"), "setTestOnly");
  private static final Node GOOG_MODULE_DECLARELEGACYNAMESPACE =
      IR.getprop(GOOG_MODULE.cloneTree(), "declareLegacyNamespace");
  private static final Node GOOG_DECLARE_MODULE_ID = IR.getprop(IR.name("goog"), "declareModuleId");

  // TODO(johnplaisted): Remove once clients have migrated to declareModuleId
  private static final Node GOOG_MODULE_DECLARNAMESPACE =
      IR.getprop(GOOG_MODULE.cloneTree(), "declareNamespace");

  private static final String TOGGLE_NAME_PREFIX = "TOGGLE_";

  /**
   * Map from module path to module. These modules represent files and thus will contain all goog
   * namespaces that are in the file. These are not the same modules in modulesByGoogNamespace.
   */
  private final Map<String, ModuleMetadata> modulesByPath = new LinkedHashMap<>();

  /**
   * Map from Closure namespace to module. These modules represent just the single namespace and
   * thus each module has only one goog namespace in its {@link ModuleMetadata#googNamespaces()}.
   * These are not the same modules in modulesByPath.
   */
  private final Map<String, ModuleMetadata> modulesByGoogNamespace = new LinkedHashMap<>();

  /** The current module being traversed. */
  private ModuleMetadataBuilder currentModule;

  /**
   * The module currentModule is nested under, if any. Modules are expected to be at most two deep
   * (a script and then a goog.loadModule call).
   */
  private @Nullable ModuleMetadataBuilder parentModule;

  /** The call to goog.loadModule we are traversing. */
  private @Nullable Node loadModuleCall;

  private final AbstractCompiler compiler;
  private final boolean processCommonJsModules;
  private final ResolutionMode moduleResolutionMode;

  public GatherModuleMetadata(
      AbstractCompiler compiler,
      boolean processCommonJsModules,
      ResolutionMode moduleResolutionMode) {
    this.compiler = compiler;
    this.processCommonJsModules = processCommonJsModules;
    this.moduleResolutionMode = moduleResolutionMode;
  }

  private class ModuleMetadataBuilder {
    private boolean ambiguous;
    private boolean hasModuleBody;
    private Node declaredModuleId;
    private Node declaresLegacyNamespace;
    final ModuleMetadata.Builder metadataBuilder;
    final LinkedHashMultiset<String> googNamespaces = LinkedHashMultiset.create();

    ModuleMetadataBuilder(Node rootNode, @Nullable ModulePath path) {
      this.metadataBuilder =
          ModuleMetadata.builder()
              .path(path)
              .rootNode(rootNode)
              .moduleType(ModuleType.SCRIPT)
              .usesClosure(false)
              .isTestOnly(false);
    }

    void moduleType(ModuleType type, NodeTraversal t, Node n) {
      checkNotNull(type);

      if (metadataBuilder.moduleType() == type) {
        return;
      }

      if (metadataBuilder.moduleType() == ModuleType.SCRIPT) {
        metadataBuilder.moduleType(type);
        return;
      }

      ambiguous = true;
      t.report(n, MIXED_MODULE_TYPE, metadataBuilder.moduleType().description, type.description);
    }

    void recordDeclareModuleId(Node declaredModuleId) {
      this.declaredModuleId = declaredModuleId;
    }

    void recordDeclareLegacyNamespace(Node declaresLegacyNamespace) {
      this.declaresLegacyNamespace = declaresLegacyNamespace;
    }

    boolean isScript() {
      return metadataBuilder.moduleType() == ModuleType.SCRIPT;
    }

    ModuleMetadata build() {
      metadataBuilder.googNamespacesBuilder().addAll(googNamespaces);
      if (!ambiguous) {
        if (hasModuleBody && metadataBuilder.moduleType() == ModuleType.SCRIPT) {
          // A script with no imports or exports, but has a module body, must be an ES module.
          metadataBuilder.moduleType(ModuleType.ES6_MODULE);
        }

        if (declaredModuleId != null && metadataBuilder.moduleType() != ModuleType.ES6_MODULE) {
          compiler.report(JSError.make(declaredModuleId, DECLARE_MODULE_ID_OUTSIDE_ES6_MODULE));
        }

        if (declaresLegacyNamespace != null) {
          if (metadataBuilder.moduleType() == ModuleType.GOOG_MODULE) {
            metadataBuilder.moduleType(ModuleType.LEGACY_GOOG_MODULE);
          } else {
            compiler.report(
                JSError.make(declaresLegacyNamespace, DECLARE_LEGACY_NAMESPACE_IN_NON_MODULE));
          }
        }
      }

      return metadataBuilder.build();
    }
  }

  private static final QualifiedName GOOG_LOADMODULE = QualifiedName.of("goog.loadModule");

  /** Traverses the AST and build a sets of {@link ModuleMetadata}s. */
  private final class Finder implements NodeTraversal.Callback {

    // Store both names and vars. Strings alone is insufficient to determine whether a name is
    // actually a toggle module (since it could have been shadowed, or may have been defined in a
    // different file), but looking up by only vars is much slower. This way we can do a fast name
    // lookup, followed by a slower var lookup only if the name is known to be a toggle module name.
    final Set<String> toggleModuleNames = new LinkedHashSet<>();
    final Set<Var> toggleModules = new LinkedHashSet<>();

    @Override
    public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
      switch (n.getToken()) {
        case SCRIPT:
          enterModule(t, n, t.getInput().getPath());
          break;
        case IMPORT:
        case EXPORT:
          visitImportOrExport(t, n);
          break;
        case CALL:
          if (n.isCall() && GOOG_LOADMODULE.matches(n.getFirstChild())) {
            loadModuleCall = n;
            enterModule(t, n, null);
          }
          break;
        case MODULE_BODY:
          currentModule.hasModuleBody = true;
          break;
        case DYNAMIC_IMPORT:
          visitDynamicImport(n);
          break;
        default:
          break;
      }

      return true;
    }

    private void visitImportOrExport(NodeTraversal t, Node importOrExport) {
      checkNotNull(currentModule);
      currentModule.moduleType(ModuleType.ES6_MODULE, t, importOrExport);
      if (importOrExport.isImport()
          // export from
          || (importOrExport.hasTwoChildren() && importOrExport.getLastChild().isStringLit())) {
        currentModule
            .metadataBuilder
            .es6ImportSpecifiersBuilder()
            .add(importOrExport.getLastChild().getString());
      }
    }

    private void visitDynamicImport(Node dynamicImport) {
      if (dynamicImport.getFirstChild().isStringLit()) {
        currentModule
            .metadataBuilder
            .es6ImportSpecifiersBuilder()
            .add(dynamicImport.getFirstChild().getString());
      }
    }

    private void enterModule(NodeTraversal t, Node n, @Nullable ModulePath path) {
      ModuleMetadataBuilder newModule = new ModuleMetadataBuilder(n, path);
      if (currentModule != null) {
        if (parentModule != null) {
          t.report(n, INVALID_NESTED_LOAD_MODULE);
        }
        parentModule = currentModule;
      }
      currentModule = newModule;
    }

    private void leaveModule() {
      checkNotNull(currentModule);
      ModuleMetadata module = currentModule.build();
      if (module.path() != null) {
        modulesByPath.put(module.path().toString(), module);
      }
      for (String namespace : module.googNamespaces()) {
        modulesByGoogNamespace.put(namespace, module);
      }
      if (parentModule != null) {
        parentModule.metadataBuilder.nestedModulesBuilder().add(module);
      }
      currentModule = parentModule;
      parentModule = null;
    }

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (processCommonJsModules && currentModule != null && currentModule.isScript()) {
        // A common JS import (call to "require") does not force a module to be rewritten as
        // commonJS. Only an export statement.
        if (ProcessCommonJSModules.isCommonJsExport(t, n, moduleResolutionMode)) {
          currentModule.moduleType(ModuleType.COMMON_JS, t, n);
          return;
        }
      }

      switch (n.getToken()) {
        case SCRIPT:
          leaveModule();
          break;
        case NAME:
          visitName(t, n);
          break;
        case CALL:
          if (loadModuleCall == n) {
            leaveModule();
            loadModuleCall = null;
          } else {
            visitGoogCall(t, n);
          }
          break;
        default:
          break;
      }
    }

    private boolean isFromGoogImport(Var goog) {
      Node nameNode = goog.getNameNode();

      // Because other tools are regex based we force importing this file as "import * as goog".
      return nameNode != null
          && nameNode.isImportStar()
          && nameNode.getString().equals("goog")
          && nameNode.getParent().getFirstChild().isEmpty()
          && nameNode.getParent().getLastChild().getString().endsWith("/goog.js");
    }

    private void visitName(NodeTraversal t, Node n) {
      String name = n.getString();
      if (toggleModuleNames.contains(name)) {
        Var nameVar = t.getScope().getVar(name);
        if (toggleModules.contains(nameVar)) {
          Node parent = n.getParent();
          if (parent.isGetProp()) {
            addToggle(t, n, parent.getString());
          } else if (!NodeUtil.isNameDeclaration(parent)) {
            t.report(
                n,
                INVALID_TOGGLE_USAGE,
                "toggle modules may not be used other than looking up properties");
          }
        }
      }
      if (!"goog".equals(name)) {
        return;
      }

      Var root = t.getScope().getVar("goog");
      if (root != null && !isFromGoogImport(root)) {
        return;
      }

      currentModule.metadataBuilder.usesClosure(true);
    }

    private void visitGoogCall(NodeTraversal t, Node n) {
      if (!n.hasChildren()
          || !n.getFirstChild().isGetProp()
          || !n.getFirstChild().isQualifiedName()) {
        return;
      }

      Node getprop = n.getFirstChild();

      Node firstProp = n.getFirstChild();

      while (firstProp.isGetProp()) {
        firstProp = firstProp.getFirstChild();
      }

      if (!firstProp.isName() || !firstProp.getString().equals("goog")) {
        return;
      }

      Var root = t.getScope().getVar("goog");

      // If this is a locally defined variable it can't be the global "goog", so exit early.
      if (root != null && root.isLocal() && !root.getScope().isModuleScope()) {
        return;
      }

      // If this is a module-level variable but wasn't created by importing goog.js, return.
      if (root != null && root.getScope().isModuleScope() && !isFromGoogImport(root)) {
        return;
      }

      // If goog is defined in this script then it does not use Closure. If this is a bundle with
      // base.js in it, then it doesn't need base.js again.
      if (root == null
          || NodeUtil.getEnclosingScript(root.getNameNode()) != NodeUtil.getEnclosingScript(n)) {
        currentModule.metadataBuilder.usesClosure(true);
      }

      if (getprop.matchesQualifiedName(GOOG_PROVIDE)) {
        currentModule.moduleType(ModuleType.GOOG_PROVIDE, t, n);
        if (n.hasTwoChildren() && n.getLastChild().isStringLit()) {
          String namespace = n.getLastChild().getString();
          addNamespace(currentModule, ModuleType.GOOG_PROVIDE, namespace, t, n);
        } else {
          t.report(n, ClosureRewriteModule.INVALID_PROVIDE_NAMESPACE);
          currentModule.metadataBuilder.usesClosure(false);
        }
      } else if (getprop.matchesQualifiedName(GOOG_MODULE)) {
        currentModule.moduleType(ModuleType.GOOG_MODULE, t, n);
        if (n.hasTwoChildren() && n.getLastChild().isStringLit()) {
          String namespace = n.getLastChild().getString();
          addNamespace(currentModule, ModuleType.GOOG_MODULE, namespace, t, n);
        } else {
          t.report(n, ClosureRewriteModule.INVALID_MODULE_ID_ARG);
          currentModule.metadataBuilder.usesClosure(false);
        }
      } else if (getprop.matchesQualifiedName(GOOG_MODULE_GET)) {
        // Look for a module named ???$2etoggles and a getprop of .TOGGLE_??? on the call's return
        if (!n.hasTwoChildren()
            || !n.getLastChild().isStringLit()
            || !n.getLastChild().getString().endsWith("$2etoggles")) {
          return; // only do anything with toggle namespaces
        }
        Node parent = n.getParent();
        if (!parent.isGetProp()) {
          t.report(
              n,
              INVALID_TOGGLE_USAGE,
              "goog.module.get of toggles module must immediately look up a single toggle");
          return;
        }
        addToggle(t, parent, parent.getString());
      } else if (getprop.matchesQualifiedName(GOOG_MODULE_DECLARELEGACYNAMESPACE)) {
        currentModule.recordDeclareLegacyNamespace(n);
      } else if (getprop.matchesQualifiedName(GOOG_DECLARE_MODULE_ID)
          || getprop.matchesQualifiedName(GOOG_MODULE_DECLARNAMESPACE)) {
        if (currentModule.declaredModuleId != null) {
          t.report(n, MULTIPLE_DECLARE_MODULE_NAMESPACE);
        }
        if (n.hasTwoChildren() && n.getLastChild().isStringLit()) {
          currentModule.recordDeclareModuleId(n);
          String namespace = n.getLastChild().getString();
          addNamespace(currentModule, ModuleType.GOOG_MODULE, namespace, t, n);
        } else {
          t.report(n, INVALID_DECLARE_MODULE_ID_CALL);
        }
      } else if (getprop.matchesQualifiedName(GOOG_REQUIRE)) {
        if (n.hasTwoChildren() && n.getLastChild().isStringLit()) {
          String namespace = n.getLastChild().getString();
          currentModule.metadataBuilder.stronglyRequiredGoogNamespacesBuilder().add(namespace);
          if (namespace.endsWith("$2etoggles")) {
            // Track imports of *.toggles.ts, which are rewritten to $2etoggles.
            Node callParent = n.getParent();
            Node lhs = callParent.getFirstChild();
            if (callParent.isDestructuringLhs()) {
              // const {TOGGLE_foo} = goog.require('foo$2etoggles');
              for (Node key : lhs.children()) {
                if (key.isStringKey()) {
                  addToggle(t, n, key.getString());
                } else {
                  t.report(n, INVALID_TOGGLE_USAGE, "must be destructured with string keys");
                }
              }
            } else if (callParent.isName()) {
              // const fooToggles = goog.require('foo$2etoggles');
              String name = callParent.getString();
              Var nameVar = t.getScope().getVar(name);
              toggleModules.add(nameVar);
              toggleModuleNames.add(name);
            } else if (currentModule.metadataBuilder.moduleType() != ModuleType.GOOG_PROVIDE) {
              // Side-effect toggle-module imports are not allowed, since they don't actually do
              // anything.  We allow it in `goog.provide()` files because there's no other way to
              // import, and since toggle modules don't declare a legacy namespace, it's unusable
              // without a `goog.module.get()` (so we can catch the toggle use there, instead).
              t.report(n, INVALID_TOGGLE_USAGE, "import must be assigned");
            }
          }
        } else {
          t.report(n, INVALID_REQUIRE_NAMESPACE);
        }
      } else if (getprop.matchesQualifiedName(GOOG_REQUIRE_TYPE)) {
        if (n.hasTwoChildren() && n.getLastChild().isStringLit()) {
          currentModule
              .metadataBuilder
              .weaklyRequiredGoogNamespacesBuilder()
              .add(n.getLastChild().getString());
        } else {
          t.report(n, INVALID_REQUIRE_TYPE);
        }
      } else if (getprop.matchesQualifiedName(GOOG_MAYBE_REQUIRE)) {
        if (n.hasTwoChildren() && n.getLastChild().isStringLit()) {
          currentModule
              .metadataBuilder
              .maybeRequiredGoogNamespacesBuilder()
              .add(n.getLastChild().getString());
        } else {
          t.report(n, INVALID_MAYBE_REQUIRE);
        }
      } else if (getprop.matchesQualifiedName(GOOG_SET_TEST_ONLY)) {
        if (n.hasOneChild() || (n.hasTwoChildren() && n.getLastChild().isStringLit())) {
          currentModule.metadataBuilder.isTestOnly(true);
        } else {
          t.report(n, INVALID_SET_TEST_ONLY);
        }
      } else if (getprop.matchesQualifiedName(GOOG_REQUIRE_DYNAMIC)) {
        if (n.hasTwoChildren() && n.getLastChild().isStringLit()) {
          currentModule
              .metadataBuilder
              .dynamicallyRequiredGoogNamespacesBuilder()
              .add(n.getLastChild().getString());
        } else {
          t.report(n, INVALID_REQUIRE_DYNAMIC);
        }
      }
    }

    /** Record a toggle usage (either a destructured import or a property lookup on a module). */
    private void addToggle(NodeTraversal t, Node n, String name) {
      if (name.startsWith(TOGGLE_NAME_PREFIX)) {
        String toggleName = name.substring(TOGGLE_NAME_PREFIX.length());
        currentModule.metadataBuilder.readTogglesBuilder().add(toggleName);
      } else {
        t.report(n, INVALID_TOGGLE_USAGE, "all toggle names must start with `TOGGLE_`");
      }
    }

    /**
     * Adds the namespaces to the module and checks if the given Closure namespace is a duplicate or
     * not.
     */
    private void addNamespace(
        ModuleMetadataBuilder module,
        ModuleType moduleType,
        String namespace,
        NodeTraversal t,
        Node n) {
      if (moduleType.equals(ModuleType.GOOG_PROVIDE)
          || moduleType.equals(ModuleType.LEGACY_GOOG_MODULE)) {
        if (!NodeUtil.isValidQualifiedName(
            compiler.getOptions().getLanguageIn().toFeatureSet(), namespace)) {
          compiler.report(JSError.make(n, INVALID_NAMESPACE_OR_MODULE_ID, namespace));
        }
      }
      if (moduleType.equals(ModuleType.GOOG_MODULE)
          || moduleType.equals(ModuleType.LEGACY_GOOG_MODULE)) {
        // non-legacy goog.modules don't technically need to be valid qualified names
        if (!isValidModuleId(namespace)) {
          compiler.report(JSError.make(n, INVALID_NAMESPACE_OR_MODULE_ID, namespace));
        }
      }

      ModuleType existingType = null;
      String existingFileSource = null;
      if (module.googNamespaces.contains(namespace)) {
        existingType = module.metadataBuilder.moduleType();
        existingFileSource = t.getSourceName();
      } else {
        ModuleMetadata existingModule = modulesByGoogNamespace.get(namespace);
        if (existingModule != null) {
          existingType = existingModule.moduleType();
          existingFileSource = existingModule.rootNode().getSourceFileName();
        }
      }
      currentModule.googNamespaces.add(namespace);
      if (existingType != null) {
        switch (existingType) {
          case ES6_MODULE:
          case GOOG_MODULE:
          case LEGACY_GOOG_MODULE:
            {
              DiagnosticType diagnostic =
                  moduleType.equals(ModuleType.GOOG_PROVIDE)
                      ? ClosurePrimitiveErrors.DUPLICATE_NAMESPACE_AND_MODULE
                      : ClosurePrimitiveErrors.DUPLICATE_MODULE;
              t.report(n, diagnostic, namespace, existingFileSource);
              return;
            }
          case GOOG_PROVIDE:
            {
              DiagnosticType diagnostic =
                  moduleType.equals(ModuleType.GOOG_PROVIDE)
                      ? ClosurePrimitiveErrors.DUPLICATE_NAMESPACE
                      : ClosurePrimitiveErrors.DUPLICATE_NAMESPACE_AND_MODULE;
              t.report(n, diagnostic, namespace, existingFileSource);
              return;
            }
          case COMMON_JS:
          case SCRIPT:
            // Fall through, error
        }
        throw new IllegalStateException("Unexpected module type: " + existingType);
      }
    }
  }

  @Override
  public void process(Node externs, Node root) {
    NodeTraversal.traverse(compiler, externs, new Finder());
    NodeTraversal.traverse(compiler, root, new Finder());
    compiler.setModuleMetadataMap(new ModuleMetadataMap(modulesByPath, modulesByGoogNamespace));
  }

  // Must match closure/base.js's goog.VALID_MODULE_RE_ & also validates that dotted segments are
  // non-empty.
  private static boolean isValidModuleId(String id) {
    for (String segment : DOT_SPLITTER.split(id)) {
      if (segment.isEmpty()) {
        return false;
      }
      for (int i = 0; i < segment.length(); i++) {
        if (!Identifiers.isIdentifierPart(segment.charAt(i))) {
          return false;
        }
      }
    }
    return NAMESPACE_SEGMENT_REGEX.matcher(id).matches();
  }

  private static final Pattern NAMESPACE_SEGMENT_REGEX =
      Pattern.compile("^[a-zA-Z_$][a-zA-Z0-9_$.]*$");

  private static final Splitter DOT_SPLITTER = Splitter.on('.');
}
